///////////////////////////////////////////////////////////////////////////////
// loadgpx.4.js
//
// Javascript object to load GPX-format GPS data into Google Maps.
//
// Copyright (C) 2006 Kaz Okuda (http://notions.okuda.ca)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
//
// If you use this script or have any questions please leave a comment
// at http://notions.okuda.ca/geotagging/projects-im-working-on/gpx-viewer/
// A link to the GPL license can also be found there.
//
///////////////////////////////////////////////////////////////////////////////
//
// History:
//    revision 1 - Initial implementation
//    revision 2 - Removed LoadGPXFileIntoGoogleMap and made it the callers
//                 responsibility.  Added more options (colour, width, delta).
//    revision 3 - Waypoint parsing now compatible with Firefox.
//    revision 4 - Upgraded to Google Maps API version 2.  Tried changing the way
//               that the map calculated the way the center and zoom level, but
//               GMAP API 2 requires that you center and zoom the map first.
//               I have left the bounding box calculations commented out in case
//               they might come in handy in the future.
//
//    5/28/2010 - Upgraded to Google Maps API v3 and refactored the file a bit.
//                          (Chris Peplin)
//    7/3/2011 - Support parse from string, add some functions(getElevations,
//                   getDistance, getOverviewPath, getPrevUrl) (Gully Chen)
//
// Author: Kaz Okuda
// URI: http://notions.okuda.ca/geotagging/projects-im-working-on/gpx-viewer/
//
// Updated for Google Maps API v3 by Chris Peplin
// Fork moved to GitHub: https://github.com/peplin/gpxviewer
//
///////////////////////////////////////////////////////////////////////////////

var R = 6378137;
google.maps.LatLng.prototype.distanceFrom = function(latlng) {
    var lat = [this.lat(), latlng.lat()]
    var lng = [this.lng(), latlng.lng()]
    var dLat = (lat[1] - lat[0]) * Math.PI / 180;
    var dLng = (lng[1] - lng[0]) * Math.PI / 180;
    var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(lat[0] * Math.PI / 180) * Math.cos(lat[1] * Math.PI / 180) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
    var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    var d = R * c;
    return Math.round(d);
}

google.maps.Polyline.prototype.getDistance = function() {
    var d = 0;
    var path = this.getPath();
    var latlng;
    for (var i = 0; i < path.getLength() - 1; i++) {
        latlng = [path.getAt(i), path.getAt(i + 1)];
        d += latlng[0].distanceFrom(latlng[1]);
    }
    return d;
}

function GPXParser(xmlDoc, map) {
    this.xmlDoc = xmlDoc;
    this.map = map;
    this.trackcolour = "#FF0000";
    this.routecolour = "#0000FF"
    this.trackwidth = 12;
    this.trackopacity = 0.5;
    this.mintrackpointdelta = 0.0001;

    this.markers = [];
    this.infowindows = [];
    this.polylines = [];
    this.elevations = [];
}

GPXParser.nodeValue = function(node) {
  var retStr="";
  if (!node) {
    return '';
  }
   if(node.nodeType==3||node.nodeType==4||node.nodeType==2){
      retStr+=node.nodeValue;
   }else if(node.nodeType==1||node.nodeType==9||node.nodeType==11){
      for(var i=0;i<node.childNodes.length;++i){
         retStr+=arguments.callee(node.childNodes[i]);
      }
   }
   return retStr;
};

GPXParser.xmlParse = function (str) {
  if (typeof ActiveXObject != 'undefined' && typeof GetObject != 'undefined') {
    var doc = new ActiveXObject('Microsoft.XMLDOM');
    doc.loadXML(str);
    return doc;
  }

  if (typeof DOMParser != 'undefined') {
    return (new DOMParser()).parseFromString(str, 'text/xml');
  }

  return createElement('div', null);
}

// Set the colour of the track line segements.
GPXParser.prototype.setTrackColour = function(colour) {
    this.trackcolour = colour;
}

// Set the width of the track line segements
GPXParser.prototype.setTrackWidth = function(width) {
    this.trackwidth = width;
}

// Set the xml document
GPXParser.prototype.setxmlstr = function(xmlstr) {
    this.xmlDoc = GPXParser.xmlParse(xmlstr);
}

// reset map
GPXParser.prototype.reset = function() {
    for(var i=0; i<this.markers.length; i++)
    {
        this.markers[i].setMap(null);
        this.markers[i] = null;
    }
    this.markers.length = 0;

    for(var i=0; i<this.infowindows.length; i++)
    {
        this.infowindows[i].setMap(null);
        this.infowindows[i] = null;
    }
    this.infowindows.length = 0;

    for(var i=0; i<this.polylines.length; i++)
    {
        this.polylines[i].setMap(null);
        this.polylines[i] = null;
    }
    this.polylines.length = 0;
    this.elevations.length = 0;
}

// Set the minimum distance between trackpoints.
// Used to cull unneeded trackpoints from map.
GPXParser.prototype.setMinTrackPointDelta = function(delta) {
    this.mintrackpointdelta = delta;
}

GPXParser.prototype.translateName = function(name) {
    if(name == "wpt") {
        return "Waypoint";
    }
    else if(name == "trkpt") {
        return "Track Point";
    }
    else if(name == "rte") {
        return "Route";
    }
    else if(name == "rtept") {
        return "Route Point";
    }
}

GPXParser.prototype.hasRoute = function(){
    return this.polylines.length > 0;
}

GPXParser.prototype.getElevations = function(max_count){
    var elevations = this.elevations;
    var max_count = max_count || 640;
    var new_elevations=[];
    var count = elevations.length;
    var _point_count = max_count;
    var valid = false;

    for(var i=0; i<count; i += Math.ceil(count/_point_count)){
        new_elevations.push(elevations[i]);
        if (!valid && elevations[i].elevation != 0){
            valid = true;
        }
    }
    if((i-Math.ceil(count/_point_count)) != (count - 1)) {
        new_elevations.push(elevations[count-1]);
    }
    return valid? new_elevations : [];
}

GPXParser.prototype.getDistance = function(){
    var d=0;
    for(var i=0; i<this.polylines.length; i++){
        d += this.polylines[i].getDistance();
    }
    return d;
}

GPXParser.prototype.getStartPos = function(){
    var count = this.polylines.length;
    if(count>0){
        return this.polylines[0].getPath().getAt(0);
    }
}

GPXParser.prototype.getEndPos = function(){
    var count = this.polylines.length;
    if(count>0){
        var path = this.polylines[count-1].getPath();
        count = path.getLength();
        if(count>0){
            return path.getAt(count-1);
        }
    }
}

GPXParser._get_path = function(max_count, polyline, distance){
    var distance = distance || polyline.getDistance();
    var _path = polyline.getPath();
    var _new_path = [];
    var count = _path.getLength();
    var _point_count = Math.floor(max_count * (polyline.getDistance()/distance));
    if (_point_count<2){
        _point_count = 1;
    }
    for(var i=0; i<count; i += Math.ceil(count/_point_count)){
        _new_path.push(_path.getAt(i));
    }
    if((i-Math.ceil(count/_point_count)) != (count - 1)) {
        _new_path.push(_path.getAt(count-1));
    }
    return _new_path;
}
GPXParser.prototype.getOverviewPath = function(distance){
    var distance = distance || this.getDistance();
    var overview_path = new Array();
    for(var i=0; i<this.polylines.length; i++){
        overview_path = overview_path.concat(GPXParser._get_path(400, this.polylines[i], distance));
    }
    return overview_path;
}

GPXParser.prototype.getPrevUrl = function(distance) {
    var distance = distance || this.getDistance();
    var path="";
    for(var i=0; i<this.polylines.length; i++){
        var _path = "path=color:0xFF0000FF|enc:"+
                       google.maps.geometry.encoding.encodePath(GPXParser._get_path(400, this.polylines[i], distance));
        var _str = ('&' + _path);
        if ((path.length + _str.length) > 1750){
            return "http://maps.google.cn/maps/api/staticmap?size=640x480&sensor=false" + path;
        }else{
            path += _str;
        }
    }

    return "http://maps.google.cn/maps/api/staticmap?size=640x480&sensor=false" + path;
}


GPXParser.prototype.createMarker = function(point) {
    var lon = parseFloat(point.getAttribute("lon"));
    var lat = parseFloat(point.getAttribute("lat"));
    var html = "";

    var pointElements = point.getElementsByTagName("html");
    if(pointElements.length > 0) {
        for(i = 0; i < pointElements.item(0).childNodes.length; i++) {
            html += pointElements.item(0).childNodes[i].nodeValue;
        }
    }
    else {
        // Create the html if it does not exist in the point.
        html = "<b>" + this.translateName(point.nodeName) + "</b><br>";
        var attributes = point.attributes;
        var attrlen = attributes.length;
        for(i = 0; i < attrlen; i++) {
            html += attributes.item(i).name + " = " +
                    attributes.item(i).nodeValue + "<br>";
        }

        if(point.hasChildNodes) {
            var children = point.childNodes;
            var childrenlen = children.length;
            for(i = 0; i < childrenlen; i++) {
                // Ignore empty nodes
                if(children[i].nodeType != 1) continue;
                if(children[i].firstChild == null) continue;
                html += children[i].nodeName + " = " +
                        children[i].firstChild.nodeValue + "<br>";
            }
        }
    }

    var marker = new google.maps.Marker({
        position: new google.maps.LatLng(lat,lon),
        map: this.map
    });

    this.markers.push(marker);

    var infowindow = new google.maps.InfoWindow({
        content: html,
        size: new google.maps.Size(50,50)
    });
    this.infowindows.push(infowindow);

    google.maps.event.addListener(marker, "click", function() {
        infowindow.open(this.map, marker);
    });
}

GPXParser.prototype.addTrackSegmentToMap = function(trackSegment, colour,
        width, tagName) {
    var tagName = tagName || "trkpt";
    var trackpoints = trackSegment.getElementsByTagName(tagName);
    if(trackpoints.length == 0) {
        return;
    }

    var pointarray = [];
    var times = [];

    // process first point
    var lastlon = parseFloat(trackpoints[0].getAttribute("lon"));
    var lastlat = parseFloat(trackpoints[0].getAttribute("lat"));
    var latlng = new google.maps.LatLng(lastlat,lastlon);
    pointarray.push(latlng);

    for(var i = 1; i < trackpoints.length; i++) {
        var lon = parseFloat(trackpoints[i].getAttribute("lon"));
        var lat = parseFloat(trackpoints[i].getAttribute("lat"));

        // Verify that this is far enough away from the last point to be used.
        var latdiff = lat - lastlat;
        var londiff = lon - lastlon;
        if(Math.sqrt(latdiff*latdiff + londiff*londiff)
                > this.mintrackpointdelta) {
            lastlon = lon;
            lastlat = lat;
            latlng = new google.maps.LatLng(lat,lon);
            pointarray.push(latlng);
            var ele = {
                elevation: parseFloat(GPXParser.nodeValue(trackpoints[i].getElementsByTagName("ele")[0])),
                location: latlng
            }
            this.elevations.push(ele);

            var time = new Date(GPXParser.nodeValue(trackpoints[i].getElementsByTagName("time")[0]));
            times.push(time);
        }

    }

    var polyline = new google.maps.Polyline({
        path: pointarray,
        clickable: false,
        geodesic: true,
        strokeColor: colour,
        strokeWidth: width,
        strokeOpacity: this.trackopacity,
        map: this.map
    });

    polyline.times = times;
    this.polylines.push(polyline);
}

GPXParser.prototype.addTrackToMap = function(track, colour, width) {
    var segments = track.getElementsByTagName("trkseg");
    for(var i = 0; i < segments.length; i++) {
        var segmentlatlngbounds = this.addTrackSegmentToMap(segments[i], colour,
                width);
    }
}

GPXParser.prototype.centerAndZoom = function(trackSegment) {

    var trackSegment = trackSegment || this.xmlDoc;

    var pointlist = new Array("trkpt", "wpt", "rtept");
    var minlat = 0;
    var maxlat = 0;
    var minlon = 0;
    var maxlon = 0;

    for(var pointtype = 0; pointtype < pointlist.length; pointtype++) {

        // Center the map and zoom on the given segment.
        var trackpoints = trackSegment.getElementsByTagName(
                pointlist[pointtype]);

        // If the min and max are uninitialized then initialize them.
        if((trackpoints.length > 0) && (minlat == maxlat) && (minlat == 0)) {
            minlat = parseFloat(trackpoints[0].getAttribute("lat"));
            maxlat = parseFloat(trackpoints[0].getAttribute("lat"));
            minlon = parseFloat(trackpoints[0].getAttribute("lon"));
            maxlon = parseFloat(trackpoints[0].getAttribute("lon"));
        }

        for(var i = 0; i < trackpoints.length; i++) {
            var lon = parseFloat(trackpoints[i].getAttribute("lon"));
            var lat = parseFloat(trackpoints[i].getAttribute("lat"));

            if(lon < minlon) minlon = lon;
            if(lon > maxlon) maxlon = lon;
            if(lat < minlat) minlat = lat;
            if(lat > maxlat) maxlat = lat;
        }
    }

    if((minlat == maxlat) && (minlat == 0)) {
        this.map.setCenter(new google.maps.LatLng(49.327667, -122.942333), 14);
        this.map.setCenter(new google.maps.LatLng(49.327667, -122.942333), 14);
        return;
    }

    // Center around the middle of the points
    var centerlon = (maxlon + minlon) / 2;
    var centerlat = (maxlat + minlat) / 2;

    var bounds = new google.maps.LatLngBounds(
            new google.maps.LatLng(minlat, minlon),
            new google.maps.LatLng(maxlat, maxlon));
    this.map.setCenter(new google.maps.LatLng(centerlat, centerlon));
    this.map.fitBounds(bounds);
}

GPXParser.prototype.centerAndZoomToLatLngBounds = function(latlngboundsarray) {
    var boundingbox = new google.maps.LatLngBounds();
    for(var i = 0; i < latlngboundsarray.length; i++) {
        if(!latlngboundsarray[i].isEmpty()) {
            boundingbox.extend(latlngboundsarray[i].getSouthWest());
            boundingbox.extend(latlngboundsarray[i].getNorthEast());
        }
    }

    var centerlat = (boundingbox.getNorthEast().lat() +
            boundingbox.getSouthWest().lat()) / 2;
    var centerlng = (boundingbox.getNorthEast().lng() +
            boundingbox.getSouthWest().lng()) / 2;
    this.map.setCenter(new GLatLng(centerlat, centerlng),
            this.map.getBoundsZoomLevel(boundingbox));
}

GPXParser.prototype.addTrackpointsToMap = function() {
    var tracks = this.xmlDoc.documentElement.getElementsByTagName("trk");
    for(var i = 0; i < tracks.length; i++) {
        this.addTrackToMap(tracks[i], this.trackcolour, this.trackwidth);
    }
}

GPXParser.prototype.addRoutepointsToMap = function() {
    var rotues = this.xmlDoc.documentElement.getElementsByTagName("rte");
    for(var i = 0; i < rotues.length; i++) {
        this.addTrackSegmentToMap(rotues[i], this.routecolour, this.trackwidth,  "rtept");
    }
}

GPXParser.prototype.addWaypointsToMap = function() {
    var waypoints = this.xmlDoc.documentElement.getElementsByTagName("wpt");
    for(var i = 0; i < waypoints.length; i++) {
        this.createMarker(waypoints[i]);
    }
}
